package com.boil.util;

import com.boil.exception.BaseException;
import com.boil.exception.RequestParamException;
import com.boil.pojo.Page2Vo;
import com.boil.pojo.ParamValidVo;
import com.boil.pojo.ResultVo;
import com.boil.qz.safekavass.web.request.param.Page2Param;
import com.thoughtworks.xstream.XStream;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by ChenYong on 2016-09-21.
 * <br>
 * 主工具类。
 */
public final class Utils {
    /**
     * 日志
     */
    private static Logger log = LoggerFactory.getLogger(Utils.class);

    /**
     * 获取 UUID。
     *
     * @return UUID
     */
    public static String getUuid() {
        return UUID.randomUUID().toString().replace(Constants.MINUS_CHAR, Constants.EMPTY_STR).toLowerCase(Locale.getDefault());
    }

    /**
     * 检查邮箱格式是否正确。
     *
     * @param mail 邮箱
     * @return 格式是否正确：
     * <ol>
     * <li>true-正确；</li>
     * <li>false-错误。</li>
     * </ol>
     */
    public static boolean checkMail(String mail) {
        if (StringUtils.isBlank(mail)) {
            return false;
        }

        if ((mail.length() >= Constants.VALUE_5) && (mail.length() <= Constants.VALUE_30)) {
            return mail.matches(Constants.MAIL_RE);
        }

        return false;
    }

    /**
     * 获取邮箱后缀。
     *
     * @param mail 邮箱
     * @return 邮箱后缀
     */
    public static String getMailSuffix(String mail) {
        // 邮箱为空，或者邮箱格式错误，则返回空串
        if (!checkMail(mail)) {
            return Constants.EMPTY_STR;
        }

        return mail.substring(mail.lastIndexOf(Constants.AT_CHAR) + Constants.VALUE_1);
    }

    /**
     * 获取请求参数校验映射实体组成的 <code>List</code>。
     *
     * @param fieldErrors 错误字段组成的 <code>List</code>
     * @return 请求参数校验映射实体组成的 <code>List</code>
     */
    public static List<ParamValidVo> getRequestParamValidVos(List<FieldError> fieldErrors) {
        if (fieldErrors == null) {
            return new ArrayList<ParamValidVo>();
        }

        List<ParamValidVo> paramValidVos = new ArrayList<ParamValidVo>();

        for (FieldError error : fieldErrors) {
            ParamValidVo paramValidVo = new ParamValidVo();
            paramValidVo.setObjectName(error.getObjectName());
            paramValidVo.setCode(error.getCode());
            paramValidVo.setField(error.getField());
            paramValidVo.setDefaultMessage(error.getDefaultMessage());

            paramValidVos.add(paramValidVo);
        }

        return paramValidVos;
    }

    /**
     * 将驼峰式字符串转换为下划线分隔的字符串。
     *
     * @param camelCaseStr 驼峰式字符串
     * @return 下划线分隔的字符串，如果不符合驼峰式的字符串，将返回原字符串。
     */
    public static String parseCamelCase2UnderlineStr(String camelCaseStr) {
        if (StringUtils.isBlank(camelCaseStr)) {
            return Constants.EMPTY_STR;
        }

        // 下划线分隔的字符串缓冲区
        StringBuilder underlineStr = new StringBuilder();

        // 如果符合驼峰式字符串
        if (camelCaseStr.matches(Constants.CAMEL_CASE_RE)) {
            for (int i = Constants.VALUE_0; i < camelCaseStr.length(); i++) {
                char c = camelCaseStr.charAt(i);

                if ((c >= Constants.A_CHAR) && (c <= Constants.Z_CHAR)) {
                    underlineStr.append(Constants.UNDERLINE_CHAR + String.valueOf(c).toLowerCase(Locale.getDefault()));
                } else {
                    underlineStr.append(c);
                }
            }

            // 如果不符合驼峰式字符串
        } else {
            underlineStr.append(camelCaseStr);
        }

        return underlineStr.toString();
    }

    /**
     * URL 编码。
     *
     * @param s   字符串
     * @param enc 编码格式
     * @return 编码后的字符串
     * @throws BaseException 基础异常
     */
    public static String urlEncode(String s, String enc) throws BaseException {
        if (StringUtils.isBlank(s) || StringUtils.isBlank(enc)) {
            return Constants.EMPTY_STR;
        }

        try {
            return URLEncoder.encode(s, enc);
        } catch (UnsupportedEncodingException e) {
            throw new BaseException(String.format("URL编码时发现不支持的编码格式：%s", enc), e);
        }
    }

    /**
     * URL 解码。
     *
     * @param s   字符串
     * @param enc 解码格式
     * @return 解码后的字符串
     * @throws BaseException 基础异常
     */
    public static String urlDecode(String s, String enc) throws BaseException {
        if (StringUtils.isBlank(s) || StringUtils.isBlank(enc)) {
            return Constants.EMPTY_STR;
        }

        try {
            return URLDecoder.decode(s, enc);
        } catch (UnsupportedEncodingException e) {
            throw new BaseException(String.format("URL解码时发现不支持的编码格式：%s", enc), e);
        }
    }

    /**
     * 字符串编码。
     *
     * @param s         字符串
     * @param encodeEnc 编码格式
     * @return 解码后的字符串
     * @throws BaseException 基础异常
     */
    public static String encode(String s, String encodeEnc) throws BaseException {
        if (StringUtils.isBlank(s) || StringUtils.isBlank(encodeEnc)) {
            return Constants.EMPTY_STR;
        }

        try {
            // 编码
            String s1 = new String(s.getBytes(), encodeEnc);

            return s1;
        } catch (UnsupportedEncodingException e) {
            throw new BaseException(String.format("字符串编码时发现不支持的编码格式：%s", encodeEnc), e);
        }
    }

    /**
     * 字符串解码。
     *
     * @param s         字符串
     * @param encodeEnc 编码格式
     * @param decodeEnc 解码格式
     * @return 解码后的字符串
     * @throws BaseException 基础异常
     */
    public static String decode(String s, String encodeEnc, String decodeEnc) throws BaseException {
        if (StringUtils.isBlank(s) || StringUtils.isBlank(encodeEnc) || StringUtils.isBlank(decodeEnc)) {
            return Constants.EMPTY_STR;
        }

        try {
            // 先进行 encodeEnc 编码，再进行 decodeEnc 解码
            String s1 = new String(s.getBytes(encodeEnc), decodeEnc);

            return s1;
        } catch (UnsupportedEncodingException e) {
            throw new BaseException(String.format("字符串解码时发现不支持的编码/解码格式：%s/%s", encodeEnc, decodeEnc), e);
        }
    }

    /**
     * 判断字符串中是否包含中文。
     *
     * @param s 字符串
     * @return 是否包含中文：
     * <ol>
     * <li>true-是；</li>
     * <li>false-否。</li>
     * </ol>
     */
    public static boolean hasChinese(String s) {
        if (StringUtils.isBlank(s)) {
            return false;
        }

        Pattern p = Pattern.compile(Constants.CHINESE_1_RE);
        Matcher m = p.matcher(s);

        return m.find();
    }

    /**
     * 将字节数组转换为 16 进制字符串。
     *
     * @param bytes 字节数组
     * @return 16 进制字符串
     */
    public static String bytes2HexStr(byte[] bytes) {
        StringBuilder sb = new StringBuilder();

        if ((bytes == null) || (bytes.length <= Constants.VALUE_0)) {
            return Constants.EMPTY_STR;
        }

        for (int i = Constants.VALUE_0; i < bytes.length; i++) {
            String hs = Integer.toHexString(bytes[i] & 0xFF);

            if (hs.length() == Constants.VALUE_1) {
                sb.append(String.valueOf(Constants.VALUE_0));
            }

            sb.append(hs);
        }

        return sb.toString().toLowerCase(Locale.getDefault());
    }

    /**
     * 将 <code>List</code> 转换为用分隔符拼接的字符串。
     *
     * @param list  <code>List</code>
     * @param split 分隔符
     * @return <code>List</code> 用分隔符拼接的字符串
     */
    public static <T extends Serializable> String parseList2Str(List<T> list, String split) {
        if ((list == null) || list.isEmpty()) {
            return Constants.EMPTY_STR;
        }

        if (StringUtils.isBlank(split)) {
            split = Constants.EMPTY_STR;
        }

        StringBuilder sb = new StringBuilder();

        for (int i = Constants.VALUE_0; i < list.size(); i++) {
            T t = list.get(i);

            if (i < (list.size() - 1)) {
                sb.append(String.valueOf(t));
                sb.append(split);
            } else {
                sb.append(String.valueOf(t));
            }
        }

        return sb.toString();
    }

    /**
     * 将分隔符拼接的字符串转换为为基本类型的 <code>List</code>。
     *
     * @param splitStr 分隔符拼接的字符串
     * @param split    分隔符
     * @param clazz    基本类型的类类型
     * @return 基本类型的 <code>List</code>
     */
    public static <T extends Serializable> List<T> parseStr2List(String splitStr, String split, Class<T> clazz) {
        if (StringUtils.isBlank(splitStr) || (clazz == null)) {
            return new ArrayList<T>();
        }

        if (StringUtils.isBlank(split)) {
            split = Constants.EMPTY_STR;
        }

        String[] strs = splitStr.split(split);
        List<Serializable> ts = new ArrayList<Serializable>();

        String classSimpleName = clazz.getTypeName();

        for (String str : strs) {
            if (Constants.BYTE.equals(classSimpleName) || Constants.BYTE_.equals(classSimpleName)) {
                ts.add(Byte.valueOf(str));
            } else if (Constants.LONG.equals(classSimpleName) || Constants.LONG_.equals(classSimpleName)) {
                ts.add(Long.valueOf(str));
            } else if (Constants.SHORT.equals(classSimpleName) || Constants.SHORT_.equals(classSimpleName)) {
                ts.add(Short.valueOf(str));
            } else if (Constants.FLOAT.equals(classSimpleName) || Constants.FLOAT_.equals(classSimpleName)) {
                ts.add(Float.valueOf(str));
            } else if (Constants.STRING.equals(classSimpleName) || Constants.STRING_.equals(classSimpleName)) {
                ts.add(String.valueOf(str));
            } else if (Constants.DOUBLE.equals(classSimpleName) || Constants.DOUBLE_.equals(classSimpleName)) {
                ts.add(Double.valueOf(str));
            } else if (Constants.INTEGER.equals(classSimpleName) || Constants.INTEGER_.equals(classSimpleName)) {
                ts.add(Integer.valueOf(str));
            } else if (Constants.BOOLEAN.equals(classSimpleName) || Constants.BOOLEAN_.equals(classSimpleName)) {
                ts.add(Boolean.valueOf(str));
            } else if (Constants.CHARACTER.equals(classSimpleName) || Constants.CHARACTER_.equals(classSimpleName)) {
                ts.add(Character.valueOf(str.charAt(Constants.VALUE_0)));
            }
        }

        return (List<T>) ts;
    }

    /**
     * 将 <code>List</code> 转为数组。<br>
     * <b style="color: red;">注意：如果 <code>List</code> 为空，将返回 null。</b>
     *
     * @param list 被转换的  <code>List</code>
     * @return 数组
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] list2Array(List<T> list) {
        if ((list == null) || list.isEmpty()) {
            return null;
        }

        T[] array = (T[]) Array.newInstance(list.get(Constants.VALUE_0).getClass(), list.size());

        for (int i = Constants.VALUE_0; i < list.size(); i++) {
            array[i] = list.get(i);
        }

        return array;
    }

    /**
     * 将数组转为 <code>List</code>。
     *
     * @param array 被转换的数组
     * @return <code>List</code>
     */
    public static <T> List<T> array2List(T[] array) {
        if ((array == null) || (array.length == Constants.VALUE_0)) {
            return new ArrayList<T>();
        }

        return Arrays.asList(array);
    }

    /**
     * 采用 MD5 对字符串加密。
     *
     * @param s 被加密的字符串
     * @return 加密后的字符串
     */
    public static String md5(String s) {
        if (s == null) {
            return Constants.EMPTY_STR;
        }

        try {
            // 获取 MD5 消息摘要
            MessageDigest md5MessageDigest = MessageDigest.getInstance(Constants.MD5);

            md5MessageDigest.update(s.getBytes());

            return bytes2HexStr(md5MessageDigest.digest());
        } catch (Exception e) {
            return Constants.EMPTY_STR;
        }
    }

    /**
     * 将密码加密（即使密码相同，得到的 MD5 不相同）。
     *
     * @param password     密码
     * @param passwordSalt 密码的盐值（最好是 UUID）
     * @return 加密后的密码
     */
    public static String encodePassword(String password, String passwordSalt) {
        if (StringUtils.isBlank(password) || StringUtils.isBlank(passwordSalt)) {
            return Constants.EMPTY_STR;
        }

        return md5(String.format("%s%s", password, passwordSalt));
    }

    /**
     * 获取客户端 IP 地址。
     *
     * @param request HttpServletRequest 实例
     * @return 客户端 IP 地址
     */
    public static String getClientIp(HttpServletRequest request) {
        if (request == null) {
            return Constants.EMPTY_STR;
        }

        try {
            String ip = request.getHeader("X-Forwarded-For");

            if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
                // 多次反向代理后会有多个 ip 值，第一个 ip 才是真实 ip
                int index = ip.indexOf(Constants.COMMA_CHAR);

                if (index != Constants.VALUE_MINUS_1) {
                    ip = ip.substring(Constants.VALUE_0, index);
                }

                log.info("获取客户端 IP 地址：{}", ip);

                return ip;
            }

            ip = request.getHeader("X-Real-IP");

            if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
                log.info("获取客户端 IP 地址：{}", ip);

                return ip;
            }

            // 获取远程 ip
            ip = request.getRemoteAddr();

            // 如果是本机地址（ipv4 或 ipv6），则根据网卡取本机配置的 IP
            if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")) {
                InetAddress inet = InetAddress.getLocalHost();

                ip = inet.getHostAddress();
            }

            log.info("获取客户端 IP 地址：{}", ip);

            return ip;
        } catch (Exception e) {
            log.error("获取客户端 IP 地址出现异常：{}", e);

            return null;
        }
    }

    /**
     * 获取完整的请求 URL，包括参数。
     *
     * @param request <code>HttpServletRequest</code> 实例
     * @return 完整的请求 URL
     */
    public static String getFullRequestUrl(HttpServletRequest request) {
        if (request == null) {
            return Constants.EMPTY_STR;
        }

        StringBuffer fullUrl = request.getRequestURL();
        String paramStr = request.getQueryString();

        if (StringUtils.isNotBlank(paramStr)) {
            fullUrl.append(Constants.ASK_CHAR);
            fullUrl.append(paramStr);
        }

        return fullUrl.toString();
    }

    /**
     * 从请求中获取 DataTables 的排序信息。
     *
     * @param request 请求
     * @return 排序信息，如：[ID ASC, NAME DESC] 这样的字符串组成的集合
     */
    public static List<String> getDataTablesSortInfo(HttpServletRequest request) {
        if (request == null) {
            return new ArrayList<String>();
        }

        // 排序信息
        List<String> sortInfo = new ArrayList<String>();

        // 如果参与排序的列数不为空
        if (StringUtils.isNotBlank(request.getParameter("iSortingCols"))) {
            // 获取参与排序的列数
            int iSortingCols = Integer.parseInt(request.getParameter("iSortingCols"));

            // 如果有排序
            if (iSortingCols > Constants.VALUE_0) {
                for (int i = Constants.VALUE_0; i < iSortingCols; i++) {
                    // 获取排序的列
                    String sSortDir = request.getParameter("mDataProp_" + request.getParameter("iSortCol_" + i));
                    // 获取排序方式
                    String sSortOrder = request.getParameter("sSortDir_" + i);

                    sortInfo.add(String.format("%s %s", Utils.parseCamelCase2UnderlineStr(sSortDir), sSortOrder).toUpperCase(Locale.getDefault()));
                }
            }
        }

        return sortInfo;
    }

    /**
     * 获取 DataTables 专用分页实例。
     *
     * @param request    请求
     * @param page2Param 分页参数
     * @param idOrderBy  ID 排序字段
     * @return DataTables 专用分页实例
     */
    public static <T> Page2<T> getPage2(HttpServletRequest request, Page2Param page2Param, String idOrderBy) {
        if ((request == null) || (page2Param == null)) {
            return null;
        }

        Page2<T> page = new Page2<T>(page2Param.getiDisplayLength(), page2Param.getiDisplayStart());

        List<String> sortInfo = getDataTablesSortInfo(request);

        //----------[针对 Oracle 排序升级改造]----------//
        if (StringUtils.isNotBlank(idOrderBy)) {
            sortInfo.add(String.format("%s %s", idOrderBy.toUpperCase(Locale.getDefault()), Constants.ASC_STR));
        }
        //----------[/针对 Oracle 排序升级改造]----------//

        if (sortInfo.size() > Constants.VALUE_0) {
            page.getParamss().put(Constants.ORDER_BY_STR, sortInfo);
        }

        return page;
    }

    /**
     * 获取 DataTables 专用分页映射实例。
     *
     * @param page2Param 分页参数
     * @param page2      分页实例
     * @return DataTables 专用分页映射实例
     */
    public static <T> Page2Vo<T> getPage2Vo(Page2Param page2Param, Page2<T> page2) {
        if ((page2Param == null) || (page2 == null)) {
            return null;
        }

        Page2Vo<T> vo = new Page2Vo<T>();
        vo.setsEcho(page2Param.getsEcho());
        vo.setiTotalRecords(page2.getCount());
        vo.setiTotalDisplayRecords(page2.getCount());
        vo.setAaData(page2.getResults());

        return vo;
    }

    /**
     * 判断请求参数是否校验通过：
     * <ol>
     * <li>未抛出异常，表示请求参数校验通过；</li>
     * <li>抛出异常，表示请求参数校验不通过。</li>
     * </ol>
     *
     * @param resultVo 结果映射实例
     * @param errors   请求参数的校验结果
     * @throws RequestParamException 抛出请求参数异常
     */
    public static void hasRequestParamError(ResultVo resultVo, BindingResult errors) throws RequestParamException {
        if ((resultVo != null) && //
                (errors != null) && //
                errors.hasErrors()) {
            List<ParamValidVo> paramValidVos = getRequestParamValidVos(errors.getFieldErrors());

            resultVo.setResultCode(Constants.REQUEST_PARAM_ERROR);
            resultVo.setResultMsg(Constants.REQUEST_PARAM_ERROR_MSG);
            resultVo.setResults(paramValidVos);

            throw new RequestParamException(Constants.REQUEST_PARAM_ERROR_MSG, resultVo);
        }
    }

    /**
     * 将 Map 转换为 URL 参数。
     *
     * @param map Map 集合
     * @return URL 参数
     */
    public static String map2UrlParam(Map<String, Object> map) {
        if ((map == null) || map.isEmpty()) {
            return Constants.EMPTY_STR;
        }

        Iterator<Map.Entry<String, Object>> entryIterator = map.entrySet().iterator();
        StringBuffer paramSb = new StringBuffer();

        int i = Constants.VALUE_0;
        while (entryIterator.hasNext()) {
            Map.Entry<String, Object> entry = entryIterator.next();

            if (i == Constants.VALUE_0) {
                paramSb.append(String.format("%s%s%s", entry.getKey(), Constants.EQUALS_CHAR, ((entry.getValue() == null) ? Constants.EMPTY_STR : entry.getValue())));
            } else {
                paramSb.append(Constants.AND_CHAR);
                paramSb.append(String.format("%s%s%s", entry.getKey(), Constants.EQUALS_CHAR, ((entry.getValue() == null) ? Constants.EMPTY_STR : entry.getValue())));
            }

            i++;
        }

        return paramSb.toString();
    }

    /**
     * 当 num 的长度小于 len 时，在 num 的前面自动补 0；
     * 否则，num 不变。
     *
     * @param num 数字
     * @param len 数字字符串的长度
     * @return 在 num 的前面自动补 0 后的数字字符串
     */
    public static String getZerofillStr(int num, int len) {
        if (len > Constants.VALUE_0) {
            return String.format(("%0" + len + "d"), num);
        } else {
            return String.valueOf(num);
        }
    }

    /**
     * 将实例转换为 Xml 字符串。
     *
     * @param t 实例
     * @return Xml 字符串
     */
    public static <T> String bean2Xml(T t) {
        if (t == null) {
            return Constants.EMPTY_STR;
        }

        XStream xStream = new XStream(new MyXppDriver());

        xStream.alias(Constants.XML_STR, t.getClass());

        return xStream.toXML(t);
    }

    /**
     * 将 Xml 格式字符串转换为 Map。
     *
     * @param xml Xml 格式字符串
     * @return Map
     * @throws BaseException 抛出基础异常
     */
    public static Map<String, String> xml2Map(String xml) throws BaseException {
        try {
            Map<String, String> map = new HashMap<String, String>();

            if (StringUtils.isNotBlank(xml)) {
                // 文档
                Document doc = DocumentHelper.parseText(xml);
                // 得到 XML 根元素
                Element root = doc.getRootElement();
                // 得到根元素的所有子节点
                List<Element> es = root.elements();

                // 遍历所有子节点
                for (Element e : es) {
                    map.put(e.getName(), e.getTextTrim());
                }
            }

            return map;
        } catch (Exception e) {
            throw new BaseException("Xml格式字符串转换为Map出现异常", e);
        }
    }

    /**
     * 将输入流中的 Xml 格式字符串转换为 Map。
     *
     * @param xmlIn 输入流
     * @return Map
     * @throws BaseException 抛出微信异常
     */
    public static Map<String, String> xml2Map(InputStream xmlIn) throws BaseException {
        try {
            Map<String, String> map = new HashMap<String, String>();

            if (xmlIn != null) {
                // 读取器
                SAXReader reader = new SAXReader();
                // 读取输入流
                Document doc = reader.read(xmlIn);
                // 获取根元素
                Element root = doc.getRootElement();
                // 获取根元素的所有子节点
                List<Element> es = root.elements();

                // 遍历所有子节点
                for (Element e : es) {
                    map.put(e.getName(), e.getTextTrim());
                }
            }

            return map;
        } catch (Exception e) {
            throw new BaseException("输入流中的Xml格式字符串转换为Map出现异常", e);

            // 释放资源
        } finally {
            try {
                if (xmlIn != null) {
                    xmlIn.close();
                }
            } catch (Exception e) {

            }
        }
    }

    /**
     * 对数字四舍五入。
     *
     * @param number  数字
     * @param decimal 保留几位小数
     * @return 四舍五入后的字符串
     */
    public static String round(double number, int decimal) {
        // 格式
        StringBuffer format = new StringBuffer("#");

        for (int i = 0; i < decimal; i++) {
            if (i == 0) {
                format.append(".");
            }

            format.append("#");
        }

        DecimalFormat decimalFormat = new DecimalFormat(format.toString());

        return decimalFormat.format(number);
    }
}